package com.gitee.feizns.quickstart.web.Interceptors.hmac;

import com.gitee.feizns.dynamic.decrypt.Encrypts;
import com.gitee.feizns.quickstart.jackson.Val;
import com.gitee.feizns.quickstart.web.WebUtils;
import com.gitee.feizns.quickstart.web.ex.UnauthorizedException;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.util.Objects;
import java.util.function.Function;

/**
 * “Hash-based Message Authentication Code” 的缩写，中文通常叫
 * “基于哈希的消息认证码”。
 * 请求：
 *  Qk-AppKey
 *  Qk-Time
 *  Qk-Nonce
 *  Qk-Sign
 * 计算签名：MD5(method + ":" + path + ":" + query + ":" + body + ":" + time + ":" + nonce + ":" + getAppSecret.apply(appKey()))
 * @author feizns
 * @since 2025/10/17
 */
public class HMACInterceptor implements HandlerInterceptor {

    /**
     * 获取密钥
     */
    protected final Function<String, String> getAppSecret;

    /**
     * 过期时间
     */
    protected final Duration expireTime;

    /**
     * 拦截器
     * @param getAppSecret 获取应用密钥
     */
    public HMACInterceptor(Function<String, String> getAppSecret) {
        this.getAppSecret = getAppSecret;
        this.expireTime = Duration.ofMinutes(15);
    }

    /**
     * 拦截器
     * @param getAppSecret 获取应用密钥
     */
    public HMACInterceptor(Function<String, String> getAppSecret, Duration duration) {
        this.getAppSecret = getAppSecret;
        this.expireTime = duration;
    }

    @Override
    public boolean preHandle(@NonNull HttpServletRequest request,
                             @NonNull HttpServletResponse response,
                             @NonNull Object handler) throws Exception {
        String appKey = appKey();
        if ( StringUtils.hasText(appKey) && !verify(request) )
            throw new UnauthorizedException("签名验证失败，请检查签名.");
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    /**
     * 应用密钥
     * @return {@link String }
     */
    public String appKey() {
        return WebUtils.getReq().getHeader("Qk-AppKey");
    }

    /**
     * 获取时间
     * @return {@link String }
     */
    public String getTime() {
        String time = WebUtils.getReq().getHeader("Qk-Time");
        if ( time == null )
            throw new UnauthorizedException("时间戳不能为空.");
        long timestamp = Val.of(time).longValue();
        if ( timestamp == 0 )
            throw new UnauthorizedException("时间戳不合法.");
        long now = System.currentTimeMillis();
        if ( timestamp < (now - expireTime.toMillis()) && timestamp > (now + expireTime.toMillis()) )
            throw new UnauthorizedException("时间戳已过期.");
        return time;
    }

    /**
     * 随机数
     * @return {@link String }
     */
    public String getNonce() {
        String nonce = WebUtils.getReq().getHeader("Qk-Nonce");
        if ( nonce == null )
            throw new UnauthorizedException("随机数不能为空.");
        return nonce;
    }

    /**
     * 获取标志
     * @return {@link String }
     */
    public String getSign() {
        String sign = WebUtils.getReq().getHeader("Qk-Sign");
        if ( sign == null )
            throw new UnauthorizedException("签名不能为空.");
        return sign;
    }

    /**
     * 签名
     * @param request 请求
     * @return {@link String }
     */
    public String sign(HttpServletRequest request) {
        String method = request.getMethod();
        String path = WebUtils.getReq().getRequestURI();
        String query = Objects.toString(request.getQueryString(), "");
        String body = WebUtils.getRequestBodyAsString();
        String time = getTime();
        String nonce = getNonce();
        String stringSignTemp = String.join(":", method, path, query, body, time, nonce, getAppSecret.apply(appKey()));
        return Encrypts.md5(stringSignTemp);
    }

    /**
     * 验证
     * @param request 请求
     * @return boolean
     */
    public boolean verify(HttpServletRequest request) {
        return Objects.equals(getSign(), sign(request));
    }

}
